# event.rb - Event handler class
# Copyright (C) 2009 Akira TAGOH

# Authors:
#   Akira TAGOH  <akira@tagoh.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

require 'mutex_m'
require 'singleton'
require 'thread'
require 'prune/debug'
require 'prune/error'
require 'prune/queue'
require 'prune/pattern'
require 'prune/types'


Thread.abort_on_exception = true if $DEBUG


module PRUNE

=begin rdoc

== PRUNE::EventManager

PRUNE::EventManager is a sigleton class to handle a event.

=end

  class EventManager
    include Singleton
    include PRUNE::Debug

    Signals = {
      :Async => nil,
      :Connected => nil,
      :Disconnected => nil,
      :Loggedin => nil,
      :ListenerLoggedin => nil,
      :Received => PRUNE::PATTERN::Commands.map {|n| n.split('|')}.flatten.push(*PRUNE::PATTERN::Responses.map {|n| n.split(',')[0]}),
      :Sent => PRUNE::PATTERN::Commands.map {|n| n.split('|')}.flatten.push(*PRUNE::PATTERN::Responses.map {|n| n.split(',')[0]}),
      :LoadPlugin => nil,
      :Reloaded => nil,
      :LoadedPlugin => nil,
      :UserDefined => [],
    }

    PRIOR_SYS_SYNC_HIGH = 1
    PRIOR_SYS_SYNC_NORMAL = 5
    PRIOR_SYS_SYNC_LOW = 7
    PRIOR_SYS_SYNC_LAST = 9
    PRIOR_USR_SYNC_HIGH = 10
    PRIOR_USR_SYNC_NORMAL = 30
    PRIOR_USR_SYNC_LOW = 40
    PRIOR_USR_SYNC_LAST = 49
    PRIOR_SYS_ASYNC_HIGH = 50
    PRIOR_SYS_ASYNC_NORMAL = 55
    PRIOR_SYS_ASYNC_LOW = 60
    PRIOR_SYS_ASYNC_LAST = 79
    PRIOR_USR_ASYNC_HIGH = 80
    PRIOR_USR_ASYNC_NORMAL = 85
    PRIOR_USR_ASYNC_LOW = 90
    PRIOR_USR_ASYNC_LAST = 100

    class << self

      def is_valid_signal?(sig)
        Signals.has_key?(sig)
      end # def is_valid_signal?(sig)

    end # class 

=begin rdoc

=== PRUNE::EventManager#new

=end

    def initialize
      @dispatchlist = {}
      @dispatchlist.extend Mutex_m
      @signal2hid = {}
      @eventq = PRUNE::Queue.new
      @thqgrp = ThreadGroup.new
      @max_thread = 10
      @is_running = false
      @thread = nil

      self.start
    end # def initialize

    attr_accessor :max_thread

=begin rdoc

=== PRUNE::EventManager#is_running?

=end

    def is_running?
      return @is_running && !@thread.nil?
    end # def is_running?

=begin rdoc

=== PRUNE::EventManager#dispatchlist(target, signal, subsignal, &block)

=end

    def dispatchlist(target, signal, subsignal, &block) # :yields: queue
      PRUNE.Fail(PRUNE::Error::InvalidSignal, signal) unless EventManager.is_valid_signal?(signal)
      PRUNE.Fail(PRUNE::Error::InvalidSubSignal, subsignal) if !subsignal.nil? && !Signals[signal].nil? && Signals[signal].class == Array && !Signals[signal].include?(subsignal.upcase) && !Signals[signal].empty?

      retval = nil
      list = nil
      @dispatchlist.synchronize do
        key = nil
        if Signals[signal].class == Array && !subsignal.nil? then
          if Signals[signal].include?(subsignal.upcase) || Signals[signal].empty? then
            key = subsignal.upcase
          end
        end
        if @signal2hid.include?(signal) && @signal2hid[signal].has_key?(key) then
          list = @signal2hid[signal][key]
        end
        unless list.nil? then
          list.sort! do |x, y|
            if !@dispatchlist.has_key?(x) then
              bug("Unknown handler id `%s' on %s", x, self)
              -1
            elsif !@dispatchlist.has_key?(y) then
              bug("Unknown handler id `%s' on %s", y, self)
            else
              dx = @dispatchlist[x]
              dy = @dispatchlist[y]
              dx.priority <=> dy.priority
            end
          end # list.sort!
          retval = list.to_a
          retval.delete_if do |x|
            y = @dispatchlist[x]
            target != y.target
          end
        else
          retval = []
        end # unless list.nil?
      end

      if block_given? then
        retval.each(&block)
      end

      retval
    end # def dispatchlist

=begin rdoc

=== PRUNE::EventManager#print_dispatchlist(target, signal, subsignal)

=end

    def print_dispatchlist(target, signal, subsignal)
      i = 1
      printf("%s(%s): target: %s\n", signal, (subsignal.nil? ? "NONE" : subsignal), target)
      dispatchlist(target, signal, subsignal) do |handler_id|
        printf("  %d. %s#%s %s [%X]\n", i,
               @dispatchlist[handler_id].instance.class,
               @dispatchlist[handler_id].function,
               (@dispatchlist[handler_id].priority < PRIOR_SYS_ASYNC_HIGH ? "SYNC" : "ASYNC"),
               handler_id)
        i += 1
      end
    end # def print_dispatchlist

=begin rdoc

=== PRUNE::EventManager#start

=end

    def start
      return true if is_running?
      @is_running = true

      @thread = Thread.new do
        while @is_running do
          unless @eventq.empty? then
            ev = @eventq.shift
            if @thqgrp.list.length > @max_thread then
              if ev.signal == :Async then
                debug('event/dispatch/async', 'too many events are now proceeding. [%d/%d]', @thqgrp.list.length, @max_thread)
              else
                debug('event/dispatch', 'too many events are now proceeding. [%d/%d]', @thqgrp.list.length, @max_thread)
              end
              @eventq.unshift(ev)
            else
              thread = Thread.new(ev) do |event|
                eid = Time.now.object_id
                if ev.signal == :Async then
                  debug('event/dispatch/async', "Signal `%s:%s' is emitting with %s as `%d'", event.signal, event.subsignal, event.data, eid)
                else
                  debug('event/dispatch', "Signal `%s:%s' is emitting with %s as `%d'", event.signal, event.subsignal, event.data, eid)
                end
                # show the order of the dispatches for the debugging purpose.
                if $DEBUG then
                  cat = self.category_list
                  if cat.list.include?('event/debug') && event.signal != :Async ||
                      cat.list.include?('event/async/debug') then
                    self.print_dispatchlist(event.target, event.signal, event.subsignal)
                  end
                end

                list = self.dispatchlist(event.target, event.signal, event.subsignal)
                until @thread.stop? do
                  sleep 0.01
                end
                @thqgrp.add(Thread.current)
                Thread.current['handler_id'] = 0
                Thread.current['id'] = eid
                Thread.current['data'] = event.data
                Thread.current['discontinued'] = false

                unless event.synchronous.nil? then
                  event.synchronous['event_thread'] = Thread.current
                  event.synchronous.wakeup
                end

                h = PRUNE::EventHandler.new(self, event.state)

                list.each do |handler_id|
                  Thread.current['handler_id'] = handler_id
                  detail = @dispatchlist[handler_id].dup

                  if detail.priority >= PRIOR_SYS_ASYNC_HIGH then
                    @thread.wakeup
                  end

                  if event.signal == :Async then
                    debug('event/signal/async', "dispatch id `%d' for a handler id `%d'[%s::%s] of %s is being brought up", eid, handler_id, detail.instance, detail.function, self)
                  else
                    debug('event/signal', "dispatch id `%d' for a handler id `%d'[%s::%s] of %s is being brought up", eid, handler_id, detail.instance, detail.function, self)
                  end

                  dupdata = []
                  if detail.args.kind_of?(Array) && detail.args[0].kind_of?(PRUNE::PluginInterface) then
                    h = detail.args[0]
                    dupdata.push(*Marshal.load(Marshal.dump(detail.args[1..-1])))
                    h.__send__(:instance_variable_set, "@state", event.state)
                  else
                    dupdata.push(*Marshal.load(Marshal.dump(detail.args)))
                  end
                  dupdata.compact!
                  event.data.each do |x|
                    y = nil
                    begin
                      y = Marshal.load(Marshal.dump(x))
                    rescue TypeError
                      y = x
                    ensure
                      dupdata << y
                    end
                  end
                  val = detail.instance.__send__(detail.function, h, event.target, *dupdata)

                  if event.signal == :Async then
                    debug('event/signal/async', "dispatch id `%d' for a handler id `%d' of %s was done with %s [result:%s]", eid, handler_id, self, val, h.result)
                  else
                    debug('event/signal', "dispatch id `%d' for a handler id `%d' of %s was done with %s [result:%s]", eid, handler_id, self, val, h.result)
                  end

                  if val == true then
                    Thread.current['discontinued'] = true
                    break
                  end
                end # l.reverse_each

                @thread.wakeup

                unless event.synchronous.nil? then
                  event.synchronous['result'] = h.result
                  event.synchronous['discontinued'] = Thread.current['discontinued']
                end

                Thread.exit
              end # Thread.new
              Thread.stop
            end # if @thqgrp.list.length > @max_thread
          end # unless @eventq.empty?
          sleep 0.01
        end
        Thread.exit
      end
    end # def start

=begin rdoc

=== PRUNE::EventManager#stop

=end

    def stop
      @is_running = false
      @thread.join
      @thread = nil
    end # def stop

=begin rdoc

=== PRUNE::EventManager#auto_registration(target, obj, prefix, prior = PRUNE::EventManager::PRIOR_USR_ASYNC_NORMAL, *args)

=end

    def auto_registration(target, obj, prefix, prior = PRUNE::EventManager::PRIOR_USR_ASYNC_NORMAL, *args)
      retval = {}
      retval[:Async] = register(target, :Async, prior, obj, sprintf("%s_async", prefix), *args) if obj.respond_to?(sprintf("%s_async", prefix))
      retval[:Connected] = register(target, :Connected, prior, obj, sprintf("%s_connected", prefix), *args) if obj.respond_to?(sprintf("%s_connected", prefix))
      retval[:Disconnected] = register(target, :Disconnected, prior, obj, sprintf("%s_disconnected", prefix, *args)) if obj.respond_to?(sprintf("%s_disconnected", prefix))
      retval[:Loggedin] = register(target, :Loggedin, prior, obj, sprintf("%s_loggedin", prefix), *args) if obj.respond_to?(sprintf("%s_loggedin", prefix))
      retval[:ListenerLoggedin] = register(target, :ListenerLoggedin, prior, obj, sprintf("%s_lloggedin", prefix), *args) if obj.respond_to?(sprintf("%s_lloggedin", prefix))
      retval[:Received] = {}
      retval[:Received][nil] = register(target, :Received, prior, obj, sprintf("%s_received", prefix), *args) if obj.respond_to?(sprintf("%s_received", prefix))
      PRUNE::PATTERN::Commands.map {|n| n.split('|')}.flatten.push(*PRUNE::PATTERN::Responses.map {|n| n.split(',')[0]}).each do |cmd|
        if obj.respond_to?(sprintf("%s_%s_received", prefix, cmd.downcase)) then
          retval[:Received][PRUNE::Message.sanitize(cmd)] = register(target, :Received, prior, obj, sprintf("%s_%s_received", prefix, cmd.downcase), cmd, *args)
        end
      end
      retval[:Sent] = {}
      retval[:Sent][nil] = register(target, :Sent, prior, obj, sprintf("%s_sent", prefix), *args) if obj.respond_to?(sprintf("%s_sent", prefix))
      PRUNE::PATTERN::Commands.map {|n| n.split('|')}.flatten.push(*PRUNE::PATTERN::Responses.map {|n| n.split(',')[0]}).each do |cmd|
        if obj.respond_to?(sprintf("%s_%s_sent", prefix, cmd.downcase)) then
          retval[:Sent][PRUNE::Message.sanitize(cmd)] = register(target, :Sent, prior, obj, sprintf("%s_%s_sent", prefix, cmd.downcase), cmd, *args)
        end
      end
      retval[:LoadPlugin] = register(target, :LoadPlugin, prior, obj, sprintf("%s_loadplugin", prefix), *args) if obj.respond_to?(sprintf("%s_loadplugin", prefix))
      retval[:Reloaded] = register(target, :Reloaded, prior, obj, sprintf("%s_reloaded", prefix), *args) if obj.respond_to?(sprintf("%s_reloaded", prefix))
      retval[:LoadedPlugin] = register(target, :LoadedPlugin, prior, obj, sprintf("%s_loadedplugin", prefix), *args) if obj.respond_to?(sprintf("%s_loadedplugin", prefix))
      retval[:UserDefined] = register(target, :UserDefined, prior, obj, sprintf("%s_userdefined", prefix), *args) if obj.respond_to?(sprintf("%s_userdefined", prefix))

      retval
    end # def auto_registration

=begin rdoc

=== PRUNE::EventManager#register(target, signal, priority, instance, function, *args)

=end

    def register(target, signal, priority, instance, function, *args)
      PRUNE.Fail(PRUNE::Error::InvalidTarget, target) unless target.kind_of?(String)
      PRUNE.Fail(PRUNE::Error::InvalidSignal, signal) unless EventManager.is_valid_signal?(signal)
      PRUNE.Fail(PRUNE::Error::NoCallBackFunction, function, instance) unless instance.respond_to?(function)

      hid = Time.now.object_id
      @dispatchlist.synchronize do
        @dispatchlist[hid] = PRUNE::TYPE::DispatchStruct.new(target, hid, signal, priority, instance, function, args)
        @signal2hid[signal] = {} unless @signal2hid.has_key?(signal)
        key = nil

        if Signals[signal].class == Array && !args.empty? then
          key = args[0]
          # drop a argument.
          @dispatchlist[hid].args.shift
          unless args[0].nil? then
            if Signals[signal].include?(args[0].upcase) || Signals[signal].empty? then
              key.upcase!
            else
              warning("Unknown command `%s' is specified.", args[0])

              return 0
            end
          end
        end
        @signal2hid[signal][key] = PRUNE::Queue.new unless @signal2hid[signal].has_key?(key)
        @signal2hid[signal][key] << hid
      end

      hid
    end # def register

=begin rdoc

=== PRUNE::EventManager#auto_unregistration(hash)

=end

    def auto_unregistration(hash)
      raise TypeError, sprintf("Can't convert %s into Hash", hash.class) unless hash.kind_of?(Hash)
      Thread.exclusive do
        hash.each do |k, v|
          if v.kind_of?(Hash) then
            auto_unregistration(v)
          else
            unregister(v)
          end
        end
      end
    end # def auto_unregistration

=begin rdoc

=== PRUNE::EventManager#unregister(handler_id)

=end

    def unregister(handler_id)
      @dispatchlist.synchronize do
        unless @dispatchlist.has_key?(handler_id) then
          begin
            raise
          rescue => e
            warning("%s doesn't have a handler id `%s':\n%s", self, handler_id, e.backtrace.join("\n"))
          end
        else
          stop_emission(handler_id)
          @dispatchlist.delete(handler_id)
        end

        @signal2hid.delete_if do |sig, list|
          list.delete_if do |key, ids|
            ids.delete(handler_id)
            ids.empty?
          end
          list.empty?
        end
      end
    end # def unregister

=begin rdoc

=== PRUNE::EventManager#synchronize(mode = true)

=end

    def synchronize(mode = true)
      flag = Thread.current['on_sync']
      Thread.current['on_sync'] = (mode == true)
      yield self
      Thread.current['on_sync'] = flag
    end # def synchronize

=begin rdoc

=== PRUNE::EventManager#is_on_sync?

=end

    def is_on_sync?
      Thread.current['on_sync'] == true
    end # def is_on_sync?

=begin rdoc

=== PRUNE::EventManager#emit(target, signal, subsignal, state, *data)

=end

    def emit(target, signal, subsignal, state, *data)
      PRUNE.Fail(PRUNE::Error::InvalidTarget, target) if !target.kind_of?(String) || target.empty?
      v = PRUNE::TYPE::EventQueueStruct.new(target, signal, subsignal, state, data, is_on_sync? ? Thread.current : nil)
      @eventq << v
      if @thread.nil? then
        warning("No management thread is running")
      else
        @thread.wakeup
      end

      if is_on_sync? then
        Thread.stop
        th = Thread.current['event_thread']
        th.join

        return Thread.current['discontinued'], Thread.current['result']
      end
    end # def emit

=begin rdoc

=== PRUNE::EventManager#stop_emission(handler_id)

=end

    def stop_emission(handler_id)
      @thqgrp.list.each do |th|
        if th['handler_id'] == handler_id then
          if th['keepalive'] == true then
            info("a signal id `%d' of handler id `%d' on %s was attempted to be killing. but hold on it because of the request.", th['id'], th['handler_id'], self)
          else
            info("a signal id `%d' of handler id `%d' on %s was killed - [%s(%s)]", th['id'], th['handler_id'], self, @dispatchlist[th['handler_id']], th['data'])
            th.exit
          end
        end
      end
    end # def stop_emission

  end # class EventManager

=begin rdoc

== PRUNE::EventHandler

=end

  class EventHandler

=begin rdoc

=== PRUNE::EventHandler#new(eventmgr, state)

=end

    def initialize(eventmgr, state)
      @result = nil
      @eventmgr = eventmgr
      @state = state
    end # def initialize

    attr_accessor :result
    attr_reader :state

=begin rdoc

=== PRUNE::EventHandler#register(target, signal, priority, instance, function, *args)

=end

    def register(target, signal, priority, instance, function, *args)
      @eventmgr.register(target, signal, priority, instance, function, *args)
    end # def register

=begin rdoc

=== PRUNE::EventHandler#unregister(handler_id)

=end

    def unregister(handler_id)
      @eventmgr.unregister(handler_id)
    end # def unregister

=begin rdoc

=== PRUNE::EventHandler#synchronize(mode = true)

=end

    def synchronize(mode = true)
      @eventmgr.synchronize(mode) do |i|
        yield self
      end
    end # def synchronize

=begin rdoc

=== PRUNE::EventHandler#emit(target, signal, subsignal, *data)

=end

    def emit(target, signal, subsignal, *data)
      if signal == :Received || signal == :Sent then
        emit_message(target, signal, subsignal, *data)
      else
        @eventmgr.emit(target, signal, subsignal, @state, *data)
      end
    end # def emit

=begin rdoc

=== PRUNE::EventHandler#emit_message(target, signal, is_subsignal, message)

=end

    def emit_message(target, signal, is_subsignal, message)
      @eventmgr.emit(target, signal, is_subsignal == true ? message.command : nil, @state, message)
    end # def emit_message

=begin rdoc

=== PRUNE::EventHandler#stop_emission(handler_id)

=end

    def stop_emission(handler_id)
      @eventmgr.stop_emission(handler_id)
    end # def stop_emission

  end # class EventHandler

end # module PRUNE

if $0 == __FILE__ then
  class Foo
    def foo(*args)
      p args
    end # def foo
  end # class Foo
  class Bar
    include PRUNE::Debug

    def bar(h, *args)
      info("bar: %s", args)

      h.result = "foo"

      true
    end # def 
    def baz(h, *args)
      info("baz: %s", args)

      h.result = "bar"

      true
    end # def 

  end # class 

  m = PRUNE::EventManager.instance
  m.start
  m.max_thread = 5
#  m.category_list.add('event/signal', 'event/dispatch')
  m.category_list.add('event/debug')
  f = Foo.new
  b = Bar.new
  hid = m.register("", :Received, PRUNE::EventManager::PRIOR_SYS_SYNC_NORMAL, f, :foo)
  hid2 = m.register("", :Received, PRUNE::EventManager::PRIOR_SYS_ASYNC_NORMAL, b, :bar)
  hid3 = m.register("", :Received, PRUNE::EventManager::PRIOR_USR_SYNC_NORMAL, b, :baz)
  n = 1
  max = 20
  Thread.new do
    loop do
      if n > 40 then
        m.stop
        break
      elsif n > 30 then
        max = 40
      end
      if n > max then
        m.synchronize do
          p m.emit(nil, :Received, nil, n)
        end
        sleep 1
      else
        m.emit(nil, :Received, nil, n)
      end
      n += 1
    end
    Thread.exit
  end.join
end
